状态同步

状态同步从服务器完成到远程客户端。本地客户端没有序列化到它的数据,因为它与服务器共享场景。序列化到本地客户端的任何数据都是多余的。但SyncVar回调在本地客户端上调用。

数据不会从远程客户端同步到服务器。这是Commands的工作。

SyncVars

SyncVars是从服务器同步到客户端的NetworkBehaviour脚本的成员变量。当一个对象产生时,或者一个新玩家加入正在进行的游戏时,它们将被发送到它们可见的网络对象上的所有SyncVars的最新状态。通过使用[SyncVar]自定义属性将成员变量制作为SyncVars

    class Player : NetworkBehaviour
    {

        [SyncVar]
        int health;

        public void TakeDamage(int amount)
        {
            if (!isServer)
                return;

            health -= amount;
        }
    }

在调用OnStartClient()之前,将SyncVars的状态应用于客户端上的对象,因此保证OnStartClient()内的对象状态为最新状态。

SyncVars可以是基本类型,例如整数,字符串和浮点数。它们也可以是Unity类型,如Vector3和用户定义的结构,但是对于结构SyncVars的更新将作为单片更新发送,而不是结构内的字段更改时的增量更改。一个NetworkBehaviour脚本最多可以有32个SyncVars - 这包括SyncLists

SyncVar的值发生变化时,服务器会自动发送SycnVar更新。SyncVars不需要手动弄脏字段。

SyncLists

SyncListsSyncVars类似,但它们是值列表而不是单个值。SyncList内容包含在使用SyncVar状态的初始状态更新中。SyncLists不需要SyncVar属性,它们是特定的类。有基本类型的内置SyncList类型:

    • SyncListString
    • SyncListFloat
    • SyncListInt
    • SyncListUInt
    • SyncListBool

还有SyncListStruct可用于用户定义的结构列表。使用SyncListStruct派生类的结构可以包含基本类型,数组和常见Unity类型的成员。它们不能包含复杂的类或通用容器。

SyncLists有一个名为Callback的SyncListChanged委托,允许在列表内容发生更改时通知客户端。这个委托被调用,发生的操作类型和操作的项目索引。

    public class MyScript : NetworkBehaviour
    {
        public struct Buf
        {
            public int id;
            public string name;
            public float timer;
        }

        public class TestBufs : SyncListStruct<Buf> {}
        TestBufs m_bufs = new TestBufs();

        void BufChanged(SyncListStruct<Buf>.Operation op, int itemIndex)
        {
            Debug.Log("buf changed:" + op);
        }

        void Start()
        {
            m_bufs.Callback = BufChanged;
        }
    }

自定义序列化函数

通常,SyncVars的使用足以让脚本将其状态序列化到客户端,但有些情况下需要更复杂的序列化代码。用于SyncVar序列化的NetworkBehaviour上的虚拟功能可能会被开发人员用来执行自己的自定义序列化。这些功能是:

    public virtual bool OnSerialize(NetworkWriter writer, bool initialState);
    public virtual void OnDeSerialize(NetworkReader reader, bool initialState);

initialState标志可用于区分第一次对象序列化和何时发送增量更新。第一次将对象发送到客户端时,它必须包含完整的状态快照,但后续更新可以通过仅包含增量更改来节省带宽。请注意,当initialStatetrue时,不会调用SyncVar回调函数,仅用于增量更新。

如果一个类有SyncVars,那么这些函数的实现会自动添加到类中。所以具有SyncVars的类不能具有自定义序列化函数。

OnSerialize函数应返回true以指示应发送更新。如果它返回true,那么该脚本的脏位将设置为零,如果它返回false,则脏位不会更改。这允许对脚本进行多次更改并随时间累积,并在系统准备好时发送,而不是每个帧。

序列化流程

具有NetworkIdentity组件的游戏对象可以有多个从NetworkBehaviour派生的脚本。序列化这些对象的流程是:

在服务器上:

    • 每个NetworkBehaviour都有一个脏屏蔽。此掩码在OnSerialize中可用作syncVarDirtyBits
    • NetworkBehaviour脚本中的每个SyncVar在脏屏蔽中被分配一个位。
    • 更改SyncVars的值会导致该SyncVar的位在脏屏蔽中设置
    • 或者,调用SetDirtyBit()直接写入脏屏蔽
    • NetworkIdentity对象作为其更新循环的一部分在服务器上进行检查
    • 如果NetworkIdentity上的任何NetworkBehaviours都是脏的,则会为该对象创建一个UpdateVars数据包
    • UpdateVars数据包通过在对象上的每个NetworkBehaviour上调用OnSerialize来填充
    • 没有脏的NetworkBehaviours会为数据包的脏位写零
    • 肮脏的NetworkBehaviours会写入其肮脏的掩码,然后是已更改的SyncVars的值
    • 如果对于NetworkBehaviour而言,OnSerialize返回true,则将为该NetworkBehaviour重置脏掩码,因此它将不会再次发送,直到其值发生更改。
    • UpdateVars数据包发送到正在观察该对象的就绪客户端

在客户端:

    • 接收到一个对象的UpdateVars数据包
    • OnDeserialize函数为对象上的每个NetworkBehaviour脚本调用
    • 对象上的每个NetworkBehaviour脚本都会读取一个脏屏蔽。
    • 如果NetworkBehaviour的脏屏蔽为零,则OnDeserialize函数将不会再读取而返回
    • 如果脏掩码为非零值,则OnDeserialize函数将读取与设置的脏位相对应的SyncVars的值
    • 如果存在SyncVar回调函数,则使用从流中读取的值调用这些函数。

所以对于这个脚本:

    public class data : NetworkBehaviour
    {

        [SyncVar]
        public int int1 = 66;

        [SyncVar]
        public int int2 = 23487;

        [SyncVar]
        public string MyString = "esfdsagsdfgsdgdsfg";
    }

生成的OnSerialize函数如下所示:

    public override bool OnSerialize(NetworkWriter writer, bool forceAll)
    {
        if (forceAll)
        {
            // the first time an object is sent to a client, send all the data (and no dirty bits)
            writer.WritePackedUInt32((uint)this.int1);
            writer.WritePackedUInt32((uint)this.int2);
            writer.Write(this.MyString);
            return true;
        }
        bool wroteSyncVar = false;
        if ((base.get_syncVarDirtyBits() & 1u) != 0u)
        {
            if (!wroteSyncVar)
            {
                // write dirty bits if this is the first SyncVar written
                writer.WritePackedUInt32(base.get_syncVarDirtyBits());
                wroteSyncVar = true;
            }
            writer.WritePackedUInt32((uint)this.int1);
        }
        if ((base.get_syncVarDirtyBits() & 2u) != 0u)
        {
            if (!wroteSyncVar)
            {
                // write dirty bits if this is the first SyncVar written
                writer.WritePackedUInt32(base.get_syncVarDirtyBits());
                wroteSyncVar = true;
            }
            writer.WritePackedUInt32((uint)this.int2);
        }
        if ((base.get_syncVarDirtyBits() & 4u) != 0u)
        {
            if (!wroteSyncVar)
            {
                // write dirty bits if this is the first SyncVar written
                writer.WritePackedUInt32(base.get_syncVarDirtyBits());
                wroteSyncVar = true;
            }
            writer.Write(this.MyString);
        }

        if (!wroteSyncVar)
        {
            // write zero dirty bits if no SyncVars were written
            writer.WritePackedUInt32(0);
        }
        return wroteSyncVar;
    }

OnDeserialize函数是这样的:

    public override void OnDeserialize(NetworkReader reader, bool initialState)
    {
        if (initialState)
        {
            this.int1 = (int)reader.ReadPackedUInt32();
            this.int2 = (int)reader.ReadPackedUInt32();
            this.MyString = reader.ReadString();
            return;
        }
        int num = (int)reader.ReadPackedUInt32();
        if ((num & 1) != 0)
        {
            this.int1 = (int)reader.ReadPackedUInt32();
        }
        if ((num & 2) != 0)
        {
            this.int2 = (int)reader.ReadPackedUInt32();
        }
        if ((num & 4) != 0)
        {
            this.MyString = reader.ReadString();
        }
    }

如果NetworkBehaviour也具有序列化函数的基类,则还应该调用基类函数。

请注意,为对象状态更新创建的UpdateVar数据包在发送到客户端之前可能会在缓冲区中聚合,因此单个传输层数据包可能包含多个对象的更新。

🔚